package krao.data;

import java.util.ArrayList;
import java.util.HashSet;

import krao.reformulation.Reformulator;
import krao.test.QueryParserTest;

import org.apache.log4j.Logger;

/**
 * One conjunctive query. <br/>
 * Composed of head atom and list of body atoms. <br/>
 * Contains methods needed for perfect reformulation algorithm.
 * 
 * @author Pepe
 * @see Reformulator
 */
public class ConjQuery implements Cloneable {
	private final static Logger logger = Logger.getLogger(QueryParserTest.class);
	// private ArrayList<Atom> queryHead;
	private Atom queryHead;
	private ArrayList<Atom> queryBody;

	public ConjQuery() {
		// queryHead = new ArrayList<Atom>();
		queryBody = new ArrayList<Atom>();
	}

	public ConjQuery(Atom queryHead, ArrayList<Atom> queryBody) {
		this.queryHead = queryHead;
		this.queryBody = queryBody;
	}

	/**
	 * Replace atom with newAtom. If newAtom is null, this method performs
	 * delete.
	 * 
	 * @param atom
	 *            atom to be replaced.
	 * @param newAtom
	 *            atom to replace @atom with. If null, @atom is deleted.
	 */
	public void replace(Atom atom, Atom newAtom) {
		queryBody.remove(atom);
		if (newAtom != null) {
			queryBody.add(newAtom);
		}
	}

	@Override
	public ConjQuery clone() {
		@SuppressWarnings("unchecked")
		ConjQuery newQuery = new ConjQuery((Atom) queryHead.clone(),
				(ArrayList<Atom>) queryBody.clone());
		return newQuery;
	}

	@Override
	public String toString() {
		return String.format("CQ:\n\t%s\n\t%s\n",
				String.format("Head:%s", queryHead.toString()), toStringBody());
	}

	private String toStringBody() {
		if (queryBody.size() == 0) {
			return "Body[0]";
		}
		StringBuilder sb = new StringBuilder(String.format("Body[%s]:", queryBody.size()));
		for (Atom oneAtom : queryBody) {
			sb.append(oneAtom.toString());
			sb.append(", ");
		}
		sb.delete(sb.length() - 2, sb.length());
		return sb.toString();
	}

	public String toStringQueryForm() {
		StringBuilder sb = new StringBuilder(queryHead.toString());
		sb.append(" :- ");
		for (Atom oneAtom : queryBody) {
			sb.append(oneAtom.toString());
			sb.append(", ");
		}
		sb.delete(sb.length() - 2, sb.length());
		return sb.toString();
	}

	public void addBodyAtom(Atom atom) {
		queryBody.add(atom);
	}

	public ArrayList<Atom> getBody() {
		return queryBody;
	}

	public Atom getHead() {
		return queryHead;
	}

	public void setHead(Atom atom) {
		queryHead = atom;
	}

	/**
	 * Removes any unbound variables occurring in this query.
	 * 
	 * @return number of removed variables
	 */
	public int anonymize() {
		int numberOfRemoved = 0;
		StringBuilder sb = new StringBuilder(queryHead.getArgString());

		for (Atom atom : queryBody) {
			sb.append(atom.getArgString());
		}

		String charString = sb.toString();

		// logger.info(charString);

		HashSet<String> charsToRemove = new HashSet<String>();
		while (!charString.isEmpty()) {
			String workingChar = charString.substring(0, 1);
			// the first and last index are the same
			if (sb.indexOf(workingChar) == sb.lastIndexOf(workingChar)) {
				charsToRemove.add(workingChar);
			}
			charString = charString.replace(workingChar, "");
		}
		numberOfRemoved = charsToRemove.size();

		HashSet<Atom> atomsToRemove = new HashSet<Atom>();
		for (String charToRemove : charsToRemove) {
			// process head
			if (charToRemove.equals(queryHead.getFirstArg())) {
				if (queryHead.isClassAtom()) {
					// this query has empty head, invalid query!
					queryHead = null;
				} else {
					queryHead.setFirstArg(null);
				}
			}
			if (queryHead != null && charToRemove.equals(queryHead.getSecondArg())) {
				queryHead.setSecondArg(null);
			}
			// process body
			for (Atom atom : queryBody) {
				if (charToRemove.equals(atom.getFirstArg())) {
					if (atom.isClassAtom()) {
						atomsToRemove.add(atom);
					} else {
						atom.setFirstArg(null);
					}
				}
				if (charToRemove.equals(atom.getSecondArg())) {
					atom.setSecondArg(null);
				}
				// if we removed all info about the variables in this atom
				if (atom.isEmpty()) {
					atomsToRemove.add(atom);
				}
			}
			for (Atom atom : atomsToRemove) {
				replace(atom, null);
			}
		}
		logger.debug("CQ anonymize removed/changed " + numberOfRemoved + " atoms");
		return numberOfRemoved;
	}

	/**
	 * Reduction of queries. First, the query is anonymized, i.e. unbound
	 * variables are removed. Than the unification of atoms is performed and
	 * once completed, it's anonymized again.
	 * 
	 * The reduce step involve going through the body atoms, trying to unify
	 * each 2 and if successful, producing new atom and removing old 2 atoms.
	 * 
	 * @return number of changed atoms (first anon. + reduce + second anon.)
	 */
	public int reduce() {
		Atom[] bodyAtoms = queryBody.toArray(new Atom[0]);
		int changed = anonymize();

		for (int i = 0; i < bodyAtoms.length; i++) {
			if (bodyAtoms[i] != null) {
				for (int j = i + 1; j < bodyAtoms.length; j++) {
					Atom unifyResult = bodyAtoms[i].unify(bodyAtoms[j]);
					// these two can be unified
					if (unifyResult != null) {
						changed++;
						bodyAtoms[i] = unifyResult;
						bodyAtoms[j] = null;
					}
				}
			}
		}
		if (changed > 0) {
			queryBody.clear();
			for (int i = 0; i < bodyAtoms.length; i++) {
				if (bodyAtoms[i] != null) {
					queryBody.add(bodyAtoms[i]);
				}
			}
			// although elegant, will cause nulls in array
			// queryBody = new ArrayList<Atom>(Arrays.asList(bodyAtoms));
		}
		changed += anonymize();
		return changed;
	}

	/**
	 * A query is invalid, if it's head is null or if it contains no atoms in
	 * body.
	 * 
	 * @return true if the query is invalid, false otherwise
	 */
	public boolean isInvalid() {
		return queryHead == null || queryBody == null || queryBody.size() == 0;
	}

	@Override
	public boolean equals(Object o) {
		if (o == null) {
			return false;
		}
		ConjQuery other = (ConjQuery) o;
		// head atoms match
		if (this.getHead().equals(other.getHead())) {
			ArrayList<Atom> otherBody = other.getBody();
			// body sizes match
			if (this.getBody().size() == otherBody.size()) {
				for (Atom atom : this.getBody()) {
					if (!otherBody.contains(atom)) {
						return false;
					}
				}

				for (Atom atom : otherBody) {
					if (!this.getBody().contains(atom)) {
						return false;
					}
				}
				// all atoms from this body are contained in other body and vice
				// versa
				return true;
			}
		}
		return false;
	}

	@Override
	public int hashCode() {
		return this.toString().hashCode();
	}

	public HashSet<String> getBodyAtomNames() {
		HashSet<String> hs = new HashSet<String>();
		for (Atom atom : this.queryBody) {
			hs.add(atom.getName());
		}
		return hs;
	}

	public HashSet<String> getBodyAtomNamesWithVariables() {
		HashSet<String> hs = new HashSet<String>();
		for (Atom atom : this.queryBody) {
			if (atom.getFirstArgVariable()) {
				hs.add(atom.getName() + "." + atom.getFirstArg());
			}
			if (!atom.isClassAtom()) {
				if (atom.getSecondArgVariable()) {
					hs.add(atom.getName() + "." + atom.getSecondArg());
				}
			}

		}
		return hs;
	}
}
